/************************************************************************
                                                                     
   Xceed Ultimate ListBox for Silverlight                                                                                                                                            
   Copyright (C) 2010 Xceed Software Inc.    
                                                                     
   This program is provided to you under the terms of the GNU General Public  
   License version 2 as published by the Free Software Foundation. 
        
   This program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY, without even the implied warranty of 
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License along 
   with this program, if not, write to the Free Software Foundation, Inc., 
   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

   This program can be provided to you by Xceed Software Inc. under a
   proprietary commercial license agreement for use in non-Open Source
   projects. Visit Xceed.com to find the commercial edition of 
   Xceed Ultimate ListBox for Silverlight.                                    
                                                                      
 **********************************************************************/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;

namespace Xceed.Silverlight.Data
{
  internal class GroupDataRangeEnumerator : DataRangeEnumeratorBase
  {
    internal GroupDataRangeEnumerator(
      IEnumerable<DataRange> ranges,
      IEnumerable<GroupDescription> groupDescriptions,
      IEnumerable<SortDescription> sortDescriptions,
      IEnumerable<SortDescription> primaryKeys,
      Type userObjectType,
      Func<string, Type> getUserObjectMemberType,
      Func<GroupHandler, SortingHandler, FilteringHandler, IAsyncEnumerator<DataPath>> enumeratorCreator )
    {
      if( enumeratorCreator == null )
        throw new ArgumentNullException( "enumeratorCreator" );

      if( userObjectType == null )
        throw new ArgumentNullException( "userObjectType" );

      if( getUserObjectMemberType == null )
        throw new ArgumentNullException( "getUserObjectMemberType" );

      // At least one range must be specified to return data from the data source.
      if( ( ranges != null ) && ( ranges.Any() ) )
      {
        // Apply the grouping configuration to the child enumerator.
        GroupHandler groupHandler = new GroupHandler( null );
        groupHandler.GroupCountPolicy = GroupCountPolicy.DoNotHandleCounts;
        GroupDataRangeEnumerator.AddRange( groupHandler.GroupDescriptions, groupDescriptions );

        // Apply the sorting configuration to the child enumerator.
        SortingHandler sortHandler = new SortingHandler();
        GroupDataRangeEnumerator.AddRange( sortHandler.SortDescriptions, sortDescriptions );

        // Apply a filter on the child enumerator to get only the items that are in the data ranges.
        FilteringHandler filterHandler = new FilteringHandler();
        filterHandler.FilterExpression = GroupDataRangeEnumerator.CreateFilter( ranges, primaryKeys );
        filterHandler.FilterDataItem = GroupDataRangeEnumerator.CreateFilterPredicate( ranges, primaryKeys, userObjectType, getUserObjectMemberType );

        // Create the child enumerator that will return every items inside the data ranges.
        m_enumerator = enumeratorCreator.Invoke( groupHandler, sortHandler, filterHandler );
      }
    }


    #region HasMore Property

    protected override bool HasMore
    {
      get
      {
        return ( !m_disposed )
            && ( !m_currentPosition.IsAfterLast )
            && ( m_enumerator != null );
      }
    }

    #endregion


    public override void Reset()
    {
      this.CheckState();

      // Get back at start.
      m_currentPosition = SourcePosition.BeforeFirst;
    }

    protected override void Dispose( bool disposing )
    {
      base.Dispose( disposing );

      // Release the enumerator.
      if( m_enumerator != null )
      {
        m_enumerator.Dispose();
        m_enumerator = null;
      }

      m_currentPosition = SourcePosition.AfterLast;
      m_disposed = true;
    }

    protected override IAsyncResult BeginGetNextCore( AsyncCallback callback, object state )
    {
      JumpMoveAndFetchParameters parameters = new JumpMoveAndFetchParameters( m_currentPosition, 1, 1 );

      return m_enumerator.BeginJumpMoveAndFetch(
               parameters,
               callback,
               state );
    }

    protected override GetNextResult EndGetNextCore( IAsyncResult asyncResult )
    {
      // Simulate the end of the source if the enumerator has been disposed.
      if( m_disposed )
        return new GetNextResult();

      // Set the position for the next iteration.
      JumpMoveAndFetchResult<DataPath> jumpResult = m_enumerator.EndJumpMoveAndFetch( ( JumpMoveAndFetchAsyncResult )asyncResult );
      m_currentPosition = jumpResult.EndPositionObject;

      // Retrieve the item.
      bool hasMore = this.HasMore;
      DataPath dataPath;

      if( hasMore )
      {
        dataPath = GroupDataRangeEnumerator.GetFirst( jumpResult.FetchedItems );
      }
      else
      {
        dataPath = null;
        m_currentPosition = SourcePosition.AfterLast;
      }

      return new GetNextResult( hasMore, dataPath, true );
    }

    private static FilterExpressionBase CreateFilter(
      IEnumerable<DataRange> ranges,
      IEnumerable<SortDescription> primaryKeys )
    {
      FilterExpressionBase globalFilter = null;

      foreach( DataRange range in ranges )
      {
        Debug.Assert( range.StartPosition is SortPosition || range.StartPosition.IsOutOfBound );
        Debug.Assert( range.EndPosition is SortPosition || range.EndPosition.IsOutOfBound );

        FilterExpressionBase filter = GroupDataRangeEnumerator.CreateRangeFilter( range, primaryKeys );

        // Add the filter to the global filter.
        if( filter != null )
        {
          if( globalFilter != null )
          {
            if( range.IsSelectionRangeInclusive )
            {
              globalFilter = new OrFilterExpression( globalFilter, filter );
            }
            else
            {
              globalFilter = new AndFilterExpression( globalFilter, new NotFilterExpression( filter ) );
            }
          }
          else
          {
            globalFilter = ( range.IsSelectionRangeInclusive ) ? filter : new NotFilterExpression( filter );
          }
        }
      }

      return globalFilter;
    }

    private static Predicate<RawDataItem> CreateFilterPredicate(
      IEnumerable<DataRange> ranges,
      IEnumerable<SortDescription> primaryKeys,
      Type userObjectType,
      Func<string, Type> getUserObjectMemberType )
    {
      Predicate<RawDataItem> predicate = null;

      foreach( DataRange range in ranges )
      {
        Debug.Assert( range.StartPosition is SortPosition || range.StartPosition.IsOutOfBound );
        Debug.Assert( range.EndPosition is SortPosition || range.EndPosition.IsOutOfBound );

        // Retrieve the predicate that is applied on the data range.
        Predicate<RawDataItem> dataRangeFilter = null;
        if( range.Context.Filter != null )
        {
          Predicate<object> dataRangeFilterPredicate = range.Context.Filter;
          dataRangeFilter = ( dataItem ) => dataRangeFilterPredicate.Invoke( dataItem.Data );
        }

        // Retrieve the filter expression that is applied and delimits the data range.
        Predicate<RawDataItem> dataRangeFilterExpression = null;
        FilterExpressionBase filterExpression = GroupDataRangeEnumerator.CreateRangeFilter( range, primaryKeys );
        if( filterExpression != null )
        {
          // We must create a predicate that will filter out data items since the filter expression
          // may reference a member that isn't on the user object.
          PredicateBuilder pb = new PredicateBuilder( filterExpression );
          Func<object, bool> dataItemPredicate = pb.ToPredicate<object>( userObjectType, getUserObjectMemberType );

          dataRangeFilterExpression = ( dataItem ) => dataItemPredicate.Invoke( dataItem );
        }

        // Merge the data range filters.
        Predicate<RawDataItem> dataRangePredicate;
        if( ( dataRangeFilter != null ) && ( dataRangeFilterExpression != null ) )
        {
          dataRangePredicate = ( dataItem ) => dataRangeFilter.Invoke( dataItem ) && dataRangeFilterExpression.Invoke( dataItem );
        }
        else if( dataRangeFilter != null )
        {
          dataRangePredicate = dataRangeFilter;
        }
        else
        {
          dataRangePredicate = dataRangeFilterExpression;
        }

        // Merge the data range predicate with previous data range predicate.
        if( dataRangePredicate != null )
        {
          if( predicate != null )
          {
            Predicate<RawDataItem> oldPredicate = predicate;
            predicate = ( dataItem ) => oldPredicate.Invoke( dataItem ) || dataRangePredicate.Invoke( dataItem );
          }
          else
          {
            predicate = dataRangePredicate;
          }
        }
        // The data range returns the whole the data source.
        else
        {
          return null;
        }
      }

      return predicate;
    }

    private static void AddRange<T>( ICollection<T> collection, IEnumerable<T> values )
    {
      foreach( T value in values )
      {
        collection.Add( value );
      }
    }

    private static T GetFirst<T>( IEnumerable<T> enumerable )
    {
      return ( enumerable != null )
               ? enumerable.FirstOrDefault()
               : default( T );
    }

    private static FilterExpressionBase CreateRangeFilter(
      DataRange range,
      IEnumerable<SortDescription> primaryKeys )
    {
      // Try to create a smaller filter expression to target the range.
      FilterExpressionBase filter = GroupDataRangeEnumerator.CreateNarrowRangeFilter( range, primaryKeys );
      if( filter != null )
        return filter;

      // Create a filter expression that gets the items from the starting position.
      SortPosition startPosition = range.StartPosition as SortPosition;
      FilterExpressionBase startFilter = null;

      if( startPosition != null )
      {
        startFilter = NavigationFilterUtils.CreateFilterExpression(
                        startPosition.SortingReference,
                        startPosition.SortValues,
                        range.AffectedSortDirection == ListSortDirection.Descending,
                        ( startPosition.StartPosition == StartPosition.Before ) );
      }

      // Create a filter expression that gets the items to the end position.
      SortPosition endPosition = range.EndPosition as SortPosition;
      FilterExpressionBase endFilter = null;
      if( endPosition != null )
      {
        endFilter = NavigationFilterUtils.CreateFilterExpression(
                      endPosition.SortingReference,
                      endPosition.SortValues,
                      range.AffectedSortDirection != ListSortDirection.Descending,
                      ( endPosition.StartPosition == StartPosition.After ) );
      }

      // Merge the start and end filters.
      if( startFilter != null && endFilter != null )
      {
        filter = new AndFilterExpression( startFilter, endFilter );
      }
      else if( startFilter != null )
      {
        filter = startFilter;
      }
      else
      {
        filter = endFilter;
      }

      // Merge the global filter of the range to the filter
      // that delimits the range.
      FilterExpressionBase contextFilter = range.Context.FilterExpression;
      if( contextFilter != null )
      {
        if( filter != null )
        {
          filter = new AndFilterExpression( contextFilter, filter );
        }
        else
        {
          filter = contextFilter;
        }
      }

      return filter;
    }

    private static FilterExpressionBase CreateNarrowRangeFilter(
      DataRange range,
      IEnumerable<SortDescription> primaryKeys )
    {
      SortPosition startPosition = range.StartPosition as SortPosition;
      SortPosition endPosition = range.EndPosition as SortPosition;

      // The range isn't delimited by SortPositions.
      if( startPosition == null || endPosition == null )
        return null;

      // The range doesn't target the same set of items.
      if( !startPosition.SortingReference.SequenceEqual( endPosition.SortingReference ) )
        return null;

      // Identify the properties that are part of the primary key.
      HashSet<string> keyPropertyName = new HashSet<string>( ( from primaryKey in primaryKeys
                                                               select primaryKey.PropertyName ).Distinct() );
      // Build a list of tuple (sort description, starting value, ending value, member of primary key).
      List<Tuple<SortDescription, object, object, bool>> rangeInfos =
        startPosition.SortingReference.Zip(
          startPosition.SortValues,
          ( sort, value ) => new Tuple<SortDescription, object, bool>( sort, value, keyPropertyName.Contains( sort.PropertyName ) ) ).Zip(
            endPosition.SortValues,
            ( info, value ) => new Tuple<SortDescription, object, object, bool>( info.Item1, info.Item2, value, info.Item3 ) ).ToList();
      // Build the list of tuple (sort description, value, member of primary key) from start
      // that share the same group level value.
      List<Tuple<SortDescription, object, bool>> sameGroupLevelRangeInfos =
        ( from rangeInfo in rangeInfos.TakeWhile( ( info ) => object.Equals( info.Item2, info.Item3 ) )
          select new Tuple<SortDescription, object, bool>( rangeInfo.Item1, rangeInfo.Item2, rangeInfo.Item4 ) ).ToList();

      // The range targets items that aren't sharing a mininum of one property value.
      if( !sameGroupLevelRangeInfos.Any() )
        return null;

      // Check out if every property that is part of the primary key has a group level value
      // and if it is shared by the starting and ending position.  If it is the case, we
      // may select the desired items using only the primary keys properties.
      bool usePrimaryKeyOnly = ( !( from rangeInfo in rangeInfos.Skip( sameGroupLevelRangeInfos.Count )
                                    where rangeInfo.Item4
                                    select rangeInfo ).Any() )
                            && ( ( from rangeInfo in sameGroupLevelRangeInfos
                                   where rangeInfo.Item3
                                   select rangeInfo.Item1.PropertyName ).Distinct().Count() == keyPropertyName.Count() );
      IEnumerable<Tuple<SortDescription, object, bool>> commonRangeInfo;

      // We only need to create a filter expression that does an equality comparison
      // on the primary key properties.
      if( usePrimaryKeyOnly )
      {
        commonRangeInfo = ( from rangeInfo in sameGroupLevelRangeInfos
                            where rangeInfo.Item3
                            select rangeInfo );
      }
      else
      {
        commonRangeInfo = sameGroupLevelRangeInfos;
      }

      // Create the part of the filter expression that uses the shared group level values.
      FilterExpressionBase filter = null;
      foreach( Tuple<SortDescription, object, bool> rangeInfo in commonRangeInfo )
      {
        FilterExpressionBase filterPart = new FilterExpression(
                                            rangeInfo.Item1.PropertyName,
                                            FilterOperator.Equal,
                                            rangeInfo.Item2 );
        if( filter != null )
        {
          filter = new AndFilterExpression( filter, filterPart );
        }
        else
        {
          filter = filterPart;
        }
      }

      // Create the part of the filter expression that doesn't share the same group level values.
      if( !usePrimaryKeyOnly )
      {
        IEnumerable<Tuple<SortDescription, object, object, bool>> diffRangeInfos = rangeInfos.Skip( sameGroupLevelRangeInfos.Count );
        if( diffRangeInfos.Any() )
        {
          // Create a filter expression from the "new" start position.
          List<SortDescription> sorts = ( from rangeInfo in diffRangeInfos
                                          select rangeInfo.Item1 ).ToList();
          List<object> startValues = ( from rangeInfo in diffRangeInfos
                                       select rangeInfo.Item2 ).ToList();
          bool reverse = range.AffectedSortDirection == ListSortDirection.Descending;
          FilterExpressionBase startFilter = NavigationFilterUtils.CreateFilterExpression( sorts, startValues, reverse, ( startPosition.StartPosition == StartPosition.Before ) );

          // Create a filter expression from the "new" end position.
          List<object> endValues = ( from rangeInfo in diffRangeInfos
                                     select rangeInfo.Item3 ).ToList();
          reverse = range.AffectedSortDirection != ListSortDirection.Descending;
          FilterExpressionBase endFilter = NavigationFilterUtils.CreateFilterExpression( sorts, endValues, reverse, ( endPosition.StartPosition == StartPosition.After ) );

          filter = new AndFilterExpression( filter, startFilter, endFilter );
        }

        IEnumerable<object> remainingStartRangeValues = startPosition.SortValues.Skip( rangeInfos.Count() );        
        if( remainingStartRangeValues.Count() > 0 )
        {
          // Create a filter expression
          List<SortDescription> sorts = startPosition.SortingReference.Skip( rangeInfos.Count() ).ToList();
          List<object> startValues = remainingStartRangeValues.ToList();
          bool reverse = range.AffectedSortDirection == ListSortDirection.Descending;
          FilterExpressionBase startFilter = NavigationFilterUtils.CreateFilterExpression( sorts, startValues, reverse, ( startPosition.StartPosition == StartPosition.Before ) );

          filter = new AndFilterExpression( filter, startFilter );
        }
        IEnumerable<object> remainingEndRangeValues = endPosition.SortValues.Skip( rangeInfos.Count() );
        if( remainingEndRangeValues.Count() > 0 )
        {
          // Create a filter expression
          List<SortDescription> sorts = endPosition.SortingReference.Skip( rangeInfos.Count() ).ToList();
          List<object> endValues = remainingEndRangeValues.ToList();
          bool reverse = range.AffectedSortDirection != ListSortDirection.Descending;
          FilterExpressionBase endFilter = NavigationFilterUtils.CreateFilterExpression( sorts, endValues, reverse, ( endPosition.StartPosition == StartPosition.After ) );

          filter = new AndFilterExpression( filter, endFilter );
        }

      }

      return filter;
    }


    #region Private Fields

    private IAsyncEnumerator<DataPath> m_enumerator; //null
    private SourcePosition m_currentPosition = SourcePosition.BeforeFirst;
    private bool m_disposed; //false

    #endregion
  }
}
